﻿using System;
using System.ComponentModel;
using System.Linq;
using System.Xml;
using Notoric.Model.Collections;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;
using Notoric.Model.Validation;

namespace Notoric.Model.Elements
{
    /// <summary>
    /// Represents a chord.
    /// </summary>
    /// <remarks>
    /// A chord in this model is a durational symbol that can
    /// hold one or more tones.
    /// </remarks>
    public class ChordElement : DurationalSymbolElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="ChordElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this symbol.
        /// </para>
        /// </param>
        /// <param name="voice">
        /// <para>
        /// Type: <see cref="VoiceDescriptorElement"/>
        /// </para>
        /// <para>
        /// The descriptor of the voice that this symbol belongs to.
        /// </para>
        /// </param>
        /// <param name="duration">
        /// <para>
        /// Type: <see cref="System.UInt16"/>
        /// </para>
        /// <para>
        /// The duration of the symbol.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="owner"/> or <paramref name="voice"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ValidationException">
        /// The value of <paramref name="voice"/> or <paramref name="duration"/> is not valid.
        /// Please, refer to the documentation of the respective properties for details.
        /// </exception>
        internal ChordElement(CompositionPart packagePart, ModelElement owner, VoiceDescriptorElement voice, ushort duration)
            : base(packagePart, owner, voice, duration)
        {
            articulationAttribute.Value = ArticulationType.Legato.ToString();
            tonesCollection = new ElementCollection<ToneElement>(this, tonesElement);
            tones = new ReadOnlyCollection<ToneElement>(tonesCollection);
        }

        /// <summary>
        /// Creates a new <see cref="ChordElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this symbol.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal ChordElement(CompositionPart packagePart, ModelElement owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            articulationAttribute = element.Attributes[PackageVocabulary.ArticulationAttribute];
            
            tonesElement = element[PackageVocabulary.ToneElementName + "s", PackageVocabulary.PackageNamespaceUri];

            tonesCollection = new ElementCollection<ToneElement>(this, tonesElement);
            tones = new ReadOnlyCollection<ToneElement>(tonesCollection);
        }

        #endregion

        #region DurationalSymbolElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElementCore(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.ChordElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            articulationAttribute = document.CreateAttribute(PackageVocabulary.ArticulationAttribute);

            element.Attributes.Append(articulationAttribute);

            tonesElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.ToneElementName + "s",
                PackageVocabulary.PackageNamespaceUri
                );

            element.AppendChild(tonesElement);

            return element;
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the pitch of some tone has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// The handler first checks that the new pitch of the tone is not duplicite, if it is,
        /// the handler throws an exception. Otherwise, it removes the tone from the collection
        /// and reinserts it to the correct index.
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void Tone_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            var tone = (ToneElement)sender;
            
            if (tonesCollection.Count(t => t == tone) > 1)
                throw new ArgumentException("The chord already contains a tone with the same pitch!");

            tonesCollection.Remove(tone);
            int i = 0;
            for (; i < tonesCollection.Count && tonesCollection[i] < tone; ++i) ;
            tonesCollection.Insert(i, tone);
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates a new <see cref="ToneElement"/> and adds it to the correct index of the <see cref="Tones"/> collection.
        /// </summary>
        /// <param name="tone">
        /// <para>
        /// Type: <see cref="Tone"/>
        /// </para>
        /// <para>
        /// The <see cref="Tone"/> structure describing the added tone.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="ToneElement"/>
        /// </para>
        /// <para>
        /// The created tone.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentException">
        /// The chord already contains a tone with the same pitch.
        /// </exception>
        public ToneElement AddTone(Tone tone)
        {
            if (tonesCollection.BinarySearch(t => t.CompareTo(tone)) != -1)
                throw new ArgumentException("The chord already contains a tone with the same pitch!", "tone");

            (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());

            var newTone = new ToneElement((CompositionPart)PackagePart, this, tone);
            int i = 0;
            for (; i < tonesCollection.Count && tonesCollection[i] < newTone; ++i) ;
            tonesCollection.Insert(i, newTone);

            newTone.PropertyChanged += new PropertyChangedEventHandler(Tone_PropertyChanged);

            return newTone;
        }

        /// <summary>
        /// Adds an existing tone to the chord.
        /// </summary>
        /// <param name="tone">
        /// <para>
        /// Type: <see cref="ToneElement"/>
        /// </para>
        /// <para>
        /// The inserted tone.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="tone"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The chord already contains a tone with the same pitch.
        /// </exception>
        public void AddExistingTone(ToneElement tone)
        {
            if (tone == null)
                throw new ArgumentNullException("tone");
            if (tonesCollection.BinarySearch(t => t.CompareTo(tone)) != -1)
                throw new ArgumentException("The chord already contains a tone with the same pitch!", "tone");

            (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());

            tone.MigrateTo(this);

            int i = 0;
            for (; i < tonesCollection.Count && tonesCollection[i] < tone; ++i) ;
            tonesCollection.Insert(i, tone);

            tone.PropertyChanged += new PropertyChangedEventHandler(Tone_PropertyChanged);
        }

        /// <summary>
        /// Removes a tone from the chord.
        /// </summary>
        /// <param name="tone">
        /// <para>
        /// Type: <see cref="ToneElement"/>
        /// </para>
        /// <para>
        /// The removed tone.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="tone"/> is <see langword="null"/>.
        /// </exception>
        public void RemoveTone(ToneElement tone)
        {
            if (tone == null)
                throw new ArgumentNullException("tone");

            (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());

            tonesCollection.Remove(tone);
            tone.PropertyChanged -= Tone_PropertyChanged;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the articulation that this chord should be played with.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ArticulationType"/>
        /// </para>
        /// <para>
        /// The articulation that this chord should be played with.
        /// </para>
        /// </value>
        public ArticulationType Articulation
        {
            get { return (ArticulationType)Enum.Parse(typeof(ArticulationType), articulationAttribute.Value); }
            set
            {
                articulationAttribute.Value = value.ToString();

                RaisePropertyChanged("Articulation");
            }
        }

        /// <summary>
        /// Gets an ordered list of tones in this chord.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ReadOnlyCollection{T}"/> of <see cref="ToneElement"/>
        /// </para>
        /// <para>
        /// The collection of tones in this chord.
        /// The tones in the list are ordered from the lowest to the highest pitch.
        /// </para>
        /// </value>
        public ReadOnlyCollection<ToneElement> Tones
        {
            get { return tones; }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="Articulation"/> property.
        /// </summary>
        private XmlAttribute articulationAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Tones"/> property.
        /// </summary>
        private ReadOnlyCollection<ToneElement> tones;
        /// <summary>
        /// The collection of tones in this chord.
        /// </summary>
        private ElementCollection<ToneElement> tonesCollection;
        /// <summary>
        /// The XML element that holds the <see cref="tonesCollection"/>.
        /// </summary>
        private XmlElement tonesElement;

        #endregion
    }

    /// <summary>
    /// Describes the type of the articulation that a <see cref="ChordElement"/>
    /// should be performed with.
    /// </summary>
    public enum ArticulationType
    {
        /// <summary>
        /// Legato.
        /// </summary>
        Legato,
        /// <summary>
        /// Staccato.
        /// </summary>
        Staccato,
        /// <summary>
        /// Staccatissimo.
        /// </summary>
        Staccatissimo,
        /// <summary>
        /// Portamento.
        /// </summary>
        Portamento,
        /// <summary>
        /// Tenuto.
        /// </summary>
        Tenuto
    }
}
